﻿using System;
using System.DirectoryServices;
using System.Globalization;
using System.Linq;
using uTILLIty.ADExtractor.Core.Formatters;

namespace uTILLIty.ADExtractor.Core
{
	public class ActiveDirectoryExtractor
	{

		public static readonly FormatterFactory FormatterFactory = new FormatterFactory();

		static ActiveDirectoryExtractor()
		{
			FormatterFactory.RegisterFormatter(new SidFormatter());
			FormatterFactory.RegisterFormatter(new GuidFormatter());
			FormatterFactory.RegisterFormatter(new DateTimeFormatter());
			FormatterFactory.RegisterFormatter(new EnumFormatter<UserAccountControlTypes>("userAccountControl"));
		}

		/// <summary>
		/// Executes the search-filter against LDAP
		/// </summary>
		/// <returns>a list of entries found in LDAP</returns>
		public SearchResultCollection QueryActiveDirectory(QueryInformation query)
		{
			using (var de = GetAdConnection(query))
			{
				using (var searcher = new DirectorySearcher(de))
				{
					searcher.PropertiesToLoad.AddRange(query.PropertiesToLoad.Select(pi => pi.PropertyName).ToArray());
					searcher.ReferralChasing = ReferralChasingOption.All;
					searcher.SearchScope = SearchScope.Subtree;
					if (query.SizeLimit > 0)
					{
						searcher.SizeLimit = query.SizeLimit;
						searcher.PageSize = query.SizeLimit;
					}
					searcher.Filter = query.Query;
					try
					{
						SearchResultCollection results = searcher.FindAll();
						//TraceSource.TraceEvent(TraceEventType.Verbose, 0, NtfsStorageAdapterResources.LdapSearchResults,
						//                       member.ID, filter, results.Count);
						return results;
					}
					catch (DirectoryServicesCOMException ex)
					{
						/*System.DirectoryServices.DirectoryServicesCOMException was unhandled by user code
							Message=Anmeldung fehlgeschlagen: unbekannter Benutzername oder falsches Kennwort.
							Source=System.DirectoryServices
							ErrorCode=-2147023570
							ExtendedError=-2146893044
							ExtendedErrorMessage=8009030C: LdapErr: DSID-0C0904D0, comment: AcceptSecurityContext error, data 52e, v1db0
						*/
						if (ex.ErrorCode == -2147023570 && ex.ExtendedError == -2146893044)
							throw new ApplicationException("Invalid LDAP credentials!", ex);

						/*System.DirectoryServices.DirectoryServicesCOMException was unhandled by user code
							Message=Eine Referenzauswertung wurde vom Server zurückgesendet.
							Source=System.DirectoryServices
							ErrorCode=-2147016661
							ExtendedError=8235
							ExtendedErrorMessage=0000202B: RefErr: DSID-031007EF, data 0, 1 access points
							ref 1: 'dont.exist'
						*/
						if (ex.ErrorCode == -2147016661 && ex.ExtendedError == 8235)
							throw new ApplicationException("Invalid LDAP ConnectionString!", ex);

						throw;
					}
					catch (System.Runtime.InteropServices.COMException ex)
					{
						if (ex.ErrorCode == -2147467259)
							throw new ApplicationException("Invalid LDAP ConnectionString!", ex);
						throw;
					}
				}
			}
		}

		public static object GetValue(SearchResult item, AdPropertyInfo prop, QueryInformation query)
		{
			object value;
			switch (prop.Aggregation)
			{
				case AggregationType.Literal:
					value = prop.PropertyName;
					break;
				case AggregationType.None:
					value = (prop.PropertyName.Equals("path", StringComparison.InvariantCultureIgnoreCase))
						? item.Path
						: item.Properties[prop.PropertyName].Count > prop.SingleValueIndex ? item.Properties[prop.PropertyName][prop.SingleValueIndex] : null;
					value = TryResolveSubPropertyValue(prop, query, value);
					break;
				case AggregationType.Count:
					value = item.Properties[prop.PropertyName].Count;
					break;
				case AggregationType.All:
					var stringValue = "";
					foreach (var curValue in item.Properties[prop.PropertyName])
					{
						value = TryResolveSubPropertyValue(prop, query, curValue);
						stringValue += ((stringValue.Length > 0) ? prop.ValueSeparator : "") + value;
					}
					value = stringValue;
					break;
				default:
					throw new ArgumentOutOfRangeException();
			}

			return value;
		}

		private static object TryResolveSubPropertyValue(AdPropertyInfo prop, QueryInformation query, object value)
		{
			if (prop.SubProperties.Length == 0)
				value = FormatterFactory.TryFormat(prop.PropertyName, value, prop.ValueSeparator);
			else
				value = TryGetSubProperty(value, prop, 0, query);
			return value;
		}

		private static object TryGetSubProperty(object currentValue, AdPropertyInfo prop, int currentSubProperty, QueryInformation query)
		{
			if (currentValue == null || string.Empty.Equals(currentValue))
				return currentValue;

			var subProperty = prop.SubProperties[currentSubProperty];
			var qi = new QueryInformation
								 {
									 Query = string.Format("(&(distinguishedName={0}))", currentValue),
									 PropertiesToLoad = new[] { new AdPropertyInfo(subProperty.PropertyName) },
									 LdapConnectionString = query.LdapConnectionString,
									 Username = query.Username,
									 Password = query.Password,
									 SizeLimit = 1
								 };
			var subElements = new ActiveDirectoryExtractor().QueryActiveDirectory(qi);
			if (subElements.Count == 0)
				return currentValue;
			var item = subElements[0];
			var value = item.Properties[subProperty.PropertyName].Count > subProperty.SingleValueIndex ? item.Properties[subProperty.PropertyName][subProperty.SingleValueIndex] : null;
			if (value != null && currentSubProperty < (prop.SubProperties.Length - 1))
				value = TryGetSubProperty(value, prop, currentSubProperty + 1, query);
			value = FormatterFactory.TryFormat(subProperty.PropertyName, value, prop.ValueSeparator);
			return value;
		}

		/// <summary>
		/// Creates a new LDAP connection
		/// </summary>
		/// <returns>a new connection</returns>
		protected virtual DirectoryEntry GetAdConnection(QueryInformation query)
		{
			if (string.IsNullOrWhiteSpace(query.LdapConnectionString))
				return new DirectoryEntry();

			if (query.LdapConnectionString.StartsWith("ldap", false, CultureInfo.InvariantCulture))
				query.LdapConnectionString = "LDAP" + query.LdapConnectionString.Substring(4);

			//if user (userName) is specified, password and cr must also be specified
			return string.IsNullOrWhiteSpace(query.Username) ? new DirectoryEntry(query.LdapConnectionString) : new DirectoryEntry(query.LdapConnectionString, query.Username, query.Password);
		}
	}
}